package epidemic;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Random;

import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.Timer;


@SuppressWarnings("serial")
public class EpiFrame extends JFrame implements MouseListener, ActionListener, KeyListener {
	
	// RNG for node generation and topology
	Random rng;
	
	// The nodes of the demo
	ArrayList<PushNode> nodes;
	
	// Epidemic settings
	public static EpiSettings settings;
	
	// Number of steps performed
	int nSteps;
	
	// The selected setting to change in the ui
	int uiSelectedSetting = 0;
	
	// The image and graphics used for drawing
	BufferedImage image;
	Graphics2D g;
	
	// Timer used for animation
	Timer timer;
	
	// Menu bar
	JMenuBar menuBar;
	JMenu fileMenu;
	JMenuItem fileExitItem;
	
	public EpiFrame(EpiSettings settings) {
		// Set settings
		EpiFrame.settings = settings;

		// Setup frame
		this.setTitle("Epidemic Dissemination Demonstration");
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setResizable(false);
		
		// Enable input
		this.addMouseListener(this);
		this.addKeyListener(this);
		
		// Init the ui
		initUi();
		
		// Create the image and graphics used for drawing
		this.image = new BufferedImage(settings.width, settings.height, BufferedImage.TYPE_INT_RGB);
		this.g = (Graphics2D) image.getGraphics();
		this.add(new JLabel(new ImageIcon(image)));
		
		// Set anti-aliasing
		if (settings.aa)
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		else
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
		
		// Init the sim and paint it
		this.init();
		this.repaint();
		
		this.setVisible(true);
		this.pack();
	}
	
	public void initUi() {
		this.menuBar = new JMenuBar();
		this.setJMenuBar(this.menuBar);
		
		this.fileMenu = new JMenu("File");
		this.menuBar.add(fileMenu);

		this.fileExitItem = new JMenuItem("Exit");
		this.fileExitItem.addActionListener(this);
		this.fileMenu.add(fileExitItem);
	}
	
	public void init() {
		// Create RNG
		this.rng = new Random(settings.seed);

		// Init lists
		this.nodes = new ArrayList<PushNode>(settings.nodeCount);
		
		// Create all nodes
		for (int i = 0; i < settings.nodeCount; i++) {
			PushNode node = new PushNode(nodes, settings.nodeN, settings.nodeF);
			
			// Random positions
			double r = settings.nodeR;
			double x = this.rng.nextDouble() * (image.getWidth() - r*2) + r;
			double y = this.rng.nextDouble() * (image.getHeight() - r*2) + r;
			
			node.setPos(new Vec(x, y));
			node.setRadius(r);
			
			nodes.add(node);
		}
		
		// Infect start node
		this.nodes.get(0).infect();
		
		// Create animation timer
		this.timer = new Timer(settings.animationSpeed, this);
		
		// Reset number of steps
		this.nSteps = 0;
	}
	
	public void repaint() {
		// Clear screen
		g.setColor(Color.WHITE);
		g.fillRect(0, 0, image.getWidth(), image.getHeight());
		
		// Paint nodes
		for (PushNode node : nodes) {
			node.paint(g);
		}
		
		// Paint OSD
		
		// Get stats
		int infected = 0, susceptible = 0, removed = 0;
		for (PushNode node : nodes) {
			if (node.isInfected())
				infected++;
			else if (node.isSusceptible())
				susceptible++;
			else if (node.isRemoved())
				removed++;
		}
		
		// Setup font
		g.setColor(Color.BLACK);
		g.setFont(new Font("Monospaced", Font.PLAIN, 13));
		
		// Determine where to paint
		int x = 5, y = 13, h = 13;

		// Paint info
		EpiUtils.drawShadedString(g, "'enter' to step, 'space' to play, 'r' to reset, 's' to reseed", Color.BLACK, Color.WHITE, x, y);
		EpiUtils.drawShadedString(g, String.format("infected:    %5d %5.1f%% (red)", infected, 100.0 * infected / settings.nodeCount), Color.BLACK, Color.WHITE, x, y+=h);
		EpiUtils.drawShadedString(g, String.format("susceptible: %5d %5.1f%% (blue)", susceptible, 100.0 * susceptible / settings.nodeCount), Color.BLACK, Color.WHITE, x, y+=h);
		EpiUtils.drawShadedString(g, String.format("removed:     %5d %5.1f%% (black)", removed, 100.0 * removed / settings.nodeCount), Color.BLACK, Color.WHITE, x, y+=h);
		EpiUtils.drawShadedString(g, String.format("steps:       %5d", this.nSteps), Color.BLACK, Color.WHITE, x, y+=h);
		
		// Repaint all components (the JLabel with the image)
		super.repaint();
	}
	
	public void step() {
		// Find all infected nodes
		ArrayList<PushNode> infected = new ArrayList<PushNode>();
		for (PushNode node : nodes) {

			// Clear all recently infected lists to avoid too many arrows
			node.clearRecent();
			
			// Add infected nodes
			if (node.isInfected()) {
				infected.add(node);
			}
		}
		
		// Do not step if there are no infected nodes
		if (infected.isEmpty())
			return;
		
		// Perform steps for all infected nodes
		for (PushNode node : infected) {
			node.step();
		}
		
		// Increment number of steps
		this.nSteps++;
	}
	
	public void mouseEntered(MouseEvent e) {
		// Nothing
	}
	
	public void mouseExited(MouseEvent e) {
		// Nothing
	}
	
	public void mousePressed(MouseEvent e) {
		// Nothing
	}
	
	public void mouseReleased(MouseEvent e) {
		if (e.getButton() == MouseEvent.BUTTON1) {
			// Perform a step and paint it
			step();
			repaint();
		} else if (e.getButton() == MouseEvent.BUTTON3) {
			// Reseed, reset the sim and paint it
			settings.seed = this.rng.nextLong();
			init();
			repaint();
		}
	}
	
	public void mouseClicked(MouseEvent e) {
		// Nothing
	}

	public void actionPerformed(ActionEvent e) {
		if (e.getSource() == timer) {
			// Perform a step and paint it
			step();
			repaint();
		} else if (e.getSource() == fileExitItem) {
			System.exit(0);
		}
	}

	public void keyPressed(KeyEvent e) {
		if (e.getKeyCode() == KeyEvent.VK_SPACE) {
			if (timer.isRunning())
				timer.stop();
			else
				timer.start();
		} else if (e.getKeyCode() == KeyEvent.VK_ENTER) {
			// Perform a step and paint it
			step();
			repaint();
		} else if (e.getKeyCode() == KeyEvent.VK_R) {
			// Reset the sim and paint it
			init();
			repaint();
		} else if (e.getKeyCode() == KeyEvent.VK_S) {
			// Reseed, reset the sim and paint it
			settings.seed = this.rng.nextLong();
			init();
			repaint();
		}
		
		// Repaint
		repaint();
	}

	public void keyReleased(KeyEvent e) {
		// Nothing
	}

	public void keyTyped(KeyEvent e) {
		// Nothing
	}
	
	public static void main(String[] args) {
		// Default demo settings
		EpiSettings settings = new EpiSettings();
		
		// Parse command line options
		try {
			for (int i = 0; i < args.length; i++) {
				if (args[i].equals("-nodeCount")) {
					settings.nodeCount = Integer.parseInt(args[++i]);
				} else if (args[i].equals("-nodeN")) {
					settings.nodeN = Integer.parseInt(args[++i]);
				} else if (args[i].equals("-nodeF")) {
					settings.nodeF = Integer.parseInt(args[++i]);
				} else if (args[i].equals("-nodeR")) {
					settings.nodeR = Integer.parseInt(args[++i]);
				} else if (args[i].equals("-width")) {
					settings.width = Integer.parseInt(args[++i]);
				} else if (args[i].equals("-height")) {
					settings.height = Integer.parseInt(args[++i]);
				} else if (args[i].equals("-seed")) {
					settings.seed = Integer.parseInt(args[++i]);
				} else if (args[i].equals("-aa")) {
					settings.aa = Integer.parseInt(args[++i]) != 0;
				} else if (args[i].equals("-arrows")) {
					settings.arrows = Integer.parseInt(args[++i]) != 0;
				} else {
					usage(args);
					System.exit(-1);
				}
			}
		} catch (IndexOutOfBoundsException ex) {
			usage(args);
			System.exit(-1);
		}
		
		// Create epiframe
		new EpiFrame(settings);
	}
	
	public static void usage(String[] args) {
		System.out.printf(
				"Usage: " +
				"\n\t[-nodeCount <number of nodes>] " +
				"\n\t[-nodeN <node rounds alive>] " +
				"\n\t[-nodeF <node spread frequency>] " +
				"\n\t[-nodeR <node radius>]" +
				"\n\t[-width <frame width>]" +
				"\n\t[-height <frame height>]" +
				"\n\t[-seed <rng seed>]" +
				"\n\t[-aa <anti-alias on/off>]" +
				"\n\t[-arrows <paint arrows on/off>]" +
				"\n");
	}
}
